package lv.solcraft.replication.server.model;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import lv.solcraft.replication.server.dao.OperDataAccess;
import lv.solcraft.replication.server.dao.OperSiteDataAccess;
import lv.solcraft.replication.server.dao.OperSiteDataAccessFactory;
import net.sf.json.JSONArray;
import net.sf.json.JSONNull;
import net.sf.json.JSONObject;
import net.sf.json.util.JSONUtils;

import org.springframework.util.Assert;

import com.noelios.restlet.util.Base64;
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;
import com.sleepycat.persist.model.Relationship;
import com.sleepycat.persist.model.SecondaryKey;

@Entity(version = 4)
public class Oper {

    @PrimaryKey
    private OperId id;

    @SecondaryKey(relate = Relationship.MANY_TO_ONE)
    private Record record;

    /**
     * Duplicate date from record field for indexing purposes
     */
    @SecondaryKey(relate = Relationship.MANY_TO_ONE)
    private String table;

    /**
     * Duplicate date from originOper field for indexing purposes
     */
    @SecondaryKey(relate = Relationship.MANY_TO_ONE)
    private Long originSiteId;

    private Map<String, Field> fields = new HashMap<String, Field>();

    private Map<String, byte[]> blobFields = new HashMap<String, byte[]>();

    private Map<String, Field> infoFields = new HashMap<String, Field>();

    private OperType type;

    @SecondaryKey(relate = Relationship.MANY_TO_ONE)
    private Date dateTime;

    private transient OperDataAccess operDataAccess;

    private transient OperSiteDataAccessFactory operSiteDataAccessFactory;

    private transient Map<Long, ClientState> clientStates = new HashMap<Long, ClientState>(1);

    /** For persistence only */
    @Deprecated
    protected Oper() {
    }

    public Oper(OperId id) {
        Assert.notNull(id, "Oper id can not be null for new oper");
        this.id = id;
        this.originSiteId = id.getOriginSiteId();
        this.dateTime = new Date();
    }

    public void inject(OperDataAccess operDataAccess,
            OperSiteDataAccessFactory operSiteDataAccessFactory) {
        this.operDataAccess = operDataAccess;
        this.operSiteDataAccessFactory = operSiteDataAccessFactory;
    }

    public void addField(Field field) {
        Assert.hasText(field.getName(), "Field name is empty");
        fields.put(field.getName(), field);
    }

    public void addInfoField(Field field) {
        Assert.hasText(field.getName(), "Field name is empty");
        infoFields.put(field.getName(), field);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final Oper other = (Oper) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }

    public int fieldCount() {
        return fields.size();
    }

    public Date getDateTime() {
        return dateTime;
    }

    public Field getField(String name) {
        Field field = fields.get(name);
        Assert.notNull(field, "Field doesn't exist: " + name + ", oper " + this);
        return field;
    }

    public OperId getId() {
        return id;
    }

    public Field getInfoField(String name) {
        Field field = infoFields.get(name);
        Assert.notNull(field, "Info field doesn't exist: " + name + ", oper " + this);
        return field;
    }

    public Record getRecord() {
        return record;
    }

    public OperType getType() {
        return type;
    }

    public boolean hasField(String name) {
        return fields.get(name) != null;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

    public boolean hasInfoField(String name) {
        return infoFields.get(name) != null;
    }

    public int infoFieldCount() {
        return infoFields.size();
    }

    public void removeField(String name) {
        fields.remove(name);
    }

    public void removeInfoField(String name) {
        infoFields.remove(name);
    }

    public void setRecord(Record record) {
        Assert.notNull(record, "Oper record can not be null");
        this.record = record;
        this.table = record.getTable();
    }

    public void setType(OperType type) {
        Assert.notNull(type, "Oper type can not be null ");
        this.type = type;
    }

    @Override
    public String toString() {
        return id + ", record {" + record + "}";
    }

    public String getTable() {
        return table;
    }

    public Long getOriginSiteId() {
        return originSiteId;
    }

    public JSONObject toJSON(boolean includeBlobFields) {
        JSONObject json = new JSONObject();
        json.put("id", id.toJSON());
        json.put("record", JSONObject.fromObject(record));
        json.put("type", type);
        JSONArray fieldsJson = new JSONArray();
        for (Field field : fields.values()) {
            fieldsJson.add(field.toJSON());
        }
        json.put("fields", fieldsJson);
        JSONArray infoFieldsJson = new JSONArray();
        for (Field field : infoFields.values()) {
            infoFieldsJson.add(field.toJSON());
        }
        json.put("infoFields", infoFieldsJson);
        json.put("blobFieldsCount", blobFields.size());
        if (includeBlobFields) {
            JSONArray blobFieldsJson = new JSONArray();
            for (Entry<String, byte[]> blobField : blobFields.entrySet()) {
                JSONObject blobFieldJson = new JSONObject();
                blobFieldJson.put("name", blobField.getKey());
                blobFieldJson.put("value", blobField.getValue() == null ? JSONNull.getInstance()
                        : Base64.encode(blobField.getValue(), true));
                blobFieldsJson.add(blobFieldJson);
            }
            json.put("blobFields", blobFieldsJson);
        }
        return json;
    }

    public static Oper fromJSON(long siteId, JSONObject json) {
        JSONObject jsonOperId = json.getJSONObject("id");
        OperId operId =
                new OperId(jsonOperId.getLong("originSiteId"), jsonOperId.getLong("originOperId"));
        Oper oper = new Oper(operId);
        OperType operType = OperType.valueOf(json.getString("type"));
        oper.setType(operType);
        JSONObject jsonRecord = json.getJSONObject("record");
        Record record =
                new Record(
                        jsonRecord.getString("table"),
                        jsonRecord.getLong("recordSiteId"),
                        jsonRecord.getLong("recordId"));
        oper.setRecord(record);
        for (Object o : json.getJSONArray("fields")) {
            JSONObject jsonField = (JSONObject) o;
            Field field = Field.create(jsonField);
            oper.addField(field);
        }
        for (Object o : json.getJSONArray("infoFields")) {
            JSONObject jsonField = (JSONObject) o;
            Field field = new Field(jsonField.getString("name"), jsonField.getString("value"));
            oper.addInfoField(field);
        }
        for (Object o : json.getJSONArray("blobFields")) {
            JSONObject jsonField = (JSONObject) o;
            byte[] value = null;
            if (jsonField.has("value") && !JSONUtils.isNull(jsonField.get("value")))
                value = Base64.decode(jsonField.getString("value"));
            oper.addBlobField(jsonField.getString("name"), value);
        }
        if (json.has("states")) {
            for (Object o : json.getJSONArray("states")) {
                JSONObject jsonState = (JSONObject) o;
                ClientState clientState = ClientState.create(jsonState);
                oper.getClientStates().put(clientState.getSiteId(), clientState);
            }
        }
        return oper;
    }

    private void addBlobField(String name, byte[] value) {
        Assert.hasText(name, "Blob field name is empty");
        blobFields.put(name, value);
    }

    public boolean persistNew() {
        Assert.notNull(this.dateTime, "DateTime is null");
        Assert.notNull(this.id, " is null");
        Assert.notNull(this.id.getOriginOperId(), "Id oper id is null");
        Assert.notNull(this.id.getOriginSiteId(), "Id origin site id is null");
        Assert.notNull(this.originSiteId, "Oriding site id is null");
        Assert.notNull(this.record, "Record is null");
        Assert.notNull(this.record.getRecordId(), "Record id is null");
        Assert.notNull(this.record.getRecordSiteId(), "Record site id is null");
        Assert.notNull(this.record.getTable(), "Record table is null");
        Assert.notNull(this.table, "table is null");
        Assert.notNull(this.type, "type is null");
        return operDataAccess.persistNew(this);
    }

    public void persist() {
        operDataAccess.persist(this);
    }

    public void delete() {
        deleteAllStates();
        operDataAccess.delete(this.id);
    }

    public List<OperSite> getOperSites() {
        List<OperSite> operSites = new ArrayList<OperSite>();
        for (OperSiteDataAccess da : operSiteDataAccessFactory.getAll()) {
            OperSite operSite = da.find(id);
            if (operSite != null)
                operSites.add(operSite);
        }
        return operSites;
    }

    public List<OperSite> getOperSites(long siteId) {
        List<OperSite> operSite = new ArrayList<OperSite>();
        for (OperState operState : OperState.values()) {
            operSite.add(getOperSite(siteId, operState));
        }
        return operSite;
    }

    public OperSite getOperSite(long siteId, OperState operState) {
        return operSiteDataAccessFactory.getInstance(siteId, operState).find(this.id);
    }

    public OperSite createOperSite(long siteId, OperState operState) {
        ClientState clientState = clientStates.get(siteId);
        if (clientState != null) {
            operState = clientState.getState();
        }
        OperSite operSite = new OperSite(this.id, this.record, siteId, operState);
        if (clientState != null) {
            operSite.setMessage(clientState.getMessage());
        }
        operSite.inject(this.operDataAccess, this.operSiteDataAccessFactory);
        return operSite;
    }

    public boolean recordHadErrors(long siteId) {
        return operSiteDataAccessFactory.errorOpers(siteId).contains(this.record);
    }

    public void deleteAllStates() {
        List<OperSiteDataAccess> das = operSiteDataAccessFactory.getAll();
        for (OperSiteDataAccess da : das) {
            OperSite operSite = da.find(this.id);
            if (operSite != null)
                operSite.delete();
        }
    }

    public void deleteStates(long siteId) {
        List<OperSiteDataAccess> das = operSiteDataAccessFactory.getAll(siteId);
        for (OperSiteDataAccess da : das) {
            da.delete(id);
        }
    }

    public void removeAllFields() {
        fields.clear();
    }

    public boolean isFinished(List<Long> sites) {
        for (Long siteId : sites) {
            if (getOperSite(siteId, OperState.NEW) != null)
                return false;
            if (getOperSite(siteId, OperState.ERROR) != null)
                return false;
        }
        return true;
    }

    public Map<Long, ClientState> getClientStates() {
        return clientStates;
    }

}
